Skip to content

fix(factory): tolerate sparse/stub synced issues + canary regression check#10

Merged
khaliqgant merged 1 commit into
mainfrom
fix/factory-canonical-fallback-canary
Jun 18, 2026
Merged

fix(factory): tolerate sparse/stub synced issues + canary regression check#10
khaliqgant merged 1 commit into
mainfrom
fix/factory-canonical-fallback-canary

Conversation

@khaliqgant

@khaliqgant khaliqgant commented Jun 18, 2026

Copy link
Copy Markdown
Member

Problem

Freshly-synced Linear issues are no longer dispatched. The active-issues sync now writes a change-event STUB to the primary /linear/issues/<key>__<uuid>.json path (only created/path/externalId/ts/id), while the full — but sparse (no state.id/team/labels) — body lands at the by-id/by-uuid aliases. Factory triage read only the stub, so every fresh issue parsed as stateId=''"live state is not ready-for-agent" → never dispatched. Reproduced live with AR-305.

This is the factory-side tolerance + detection layer. The upstream root cause (Nango fetch-active-issues dropping state.id/team/labels) is fixed separately; this makes the factory robust to sparse records regardless.

Changes

  • readLinearIssueWithCanonicalFallback() — when the primary path parses without usable state (a stub), re-read the canonical by-id/by-uuid sibling, re-parsed against the original primary path so key/uuid/path stay primary-anchored (dedup/dispatch keying unchanged). Wired into #readIssue and the CLI read paths (readIssueArg, isAllowedFactoryDraft).
  • State resolver hardeningcreateFactory's fallback resolver now seeds the name→UUID map from config.linear.states, so a record with only state.name resolves a role without needing the /linear/states catalog.
  • factory canary <issue> CLI — runs the real dry-run triage path and asserts a known issue is dispatch-ready (not skipped); exits non-zero with the skip reason. This is the regression detector for sync-fidelity drift — wire it into CI/cron against a standing "Ready for Agent" canary issue.
  • Golden test reproducing the stub-primary + sparse-canonical shape.

Verification

  • factory canary AR-305 against the live mount → {ok:true, issue:"AR-305", status:"dispatched"}.
  • Full suite: 512 passed.

🤖 Generated with Claude Code


Summary by cubic

Fixes dispatch failures for freshly synced Linear issues by reading canonical by-id/by-uuid records when the primary file is a stub, and adds a factory canary CLI to catch regressions early.

  • Bug Fixes

    • Added readLinearIssueWithCanonicalFallback() to re-read canonical aliases when /linear/issues/<key>__<uuid>.json parses as a stub; keeps the original path/key/uuid for dedupe and dispatch. Wired into #readIssue, readIssueArg, and the draft-allowlist check.
    • Hardened state resolution so sparse records with only state.name are mapped to roles without the /linear/states catalog by seeding the name→UUID map from config.linear.states.
  • New Features

    • fleet factory canary <issue>: runs a real dry-run triage and exits non-zero if the issue isn’t dispatch-ready, returning the skip reason. Use this in CI/cron to detect sync-fidelity drift.

Written for commit eb3a1e8. Summary will update on new commits.

Review in cubic

…ion check

Freshly-synced Linear issues land as a change-event STUB at the primary
/linear/issues/<key>__<uuid>.json path (only created/path/externalId/ts/id),
while the full — but sparse (no state.id/team/labels) — body lands at the
by-id/by-uuid aliases. Triage read only the stub, so every fresh issue read as
"live state is not ready-for-agent" and was never dispatched (live AR-305).

- readLinearIssueWithCanonicalFallback(): when the primary parses without usable
  state, re-read the canonical by-id/by-uuid sibling (re-parsed against the
  original primary path so key/uuid/path stay primary-anchored). Wired into
  #readIssue and the CLI read paths (readIssueArg, isAllowedFactoryDraft).
- createFactory's fallback state resolver now seeds the name->UUID map from
  config.linear.states, so sparse records (state.name, no state.id) resolve a
  role without the /linear/states catalog.
- New `factory canary <issue>` CLI: runs the real dry-run triage path and asserts
  a known issue is dispatch-ready (not skipped) — the regression detector for
  sync-fidelity drift. Exits non-zero with the skip reason.
- Golden test reproducing the stub-primary + sparse-canonical shape.

Verified live: `factory canary AR-305` => {ok:true, status:"dispatched"}.
512 tests pass.
@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@khaliqgant, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 42 minutes and 1 second. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 07b9b4a3-84fd-4568-be61-4ca6f5c209d5

📥 Commits

Reviewing files that changed from the base of the PR and between 872f75a and eb3a1e8.

📒 Files selected for processing (6)
  • src/cli/fleet.ts
  • src/constants/linear.ts
  • src/index.ts
  • src/orchestrator/factory.test.ts
  • src/orchestrator/factory.ts
  • src/orchestrator/index.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/factory-canonical-fallback-canary

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new factory-canary CLI command to act as a regression detector for sync-fidelity drift, ensuring known issues are correctly classified. It also implements readLinearIssueWithCanonicalFallback to handle sparse synced records (stubs) by falling back to canonical by-id or by-uuid paths. Feedback suggests validating the parsed key in canonicalIssueRecordPaths against ISSUE_KEY_PATTERN to prevent generating invalid lookup paths when processing by-uuid aliases.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +3557 to +3564
const canonicalIssueRecordPaths = (path: string): string[] => {
const key = keyFromPath(path)
const uuid = uuidFromPath(path)
return [
...(key ? [linearByIdPath(key)] : []),
...(uuid ? [linearByUuidPath(uuid)] : []),
].filter((candidate) => candidate !== path)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In canonicalIssueRecordPaths, if the path is a by-uuid alias (e.g., /linear/issues/by-uuid/uuid-7.json), keyFromPath will return uuid-7. This results in generating an invalid lookup path /linear/issues/by-id/uuid-7.json since uuid-7 is not a valid human-readable Linear key.\n\nTo avoid unnecessary network/I/O requests to the mount client for invalid paths, we should validate that the parsed key matches the expected ISSUE_KEY_PATTERN before adding the by-id path.

const canonicalIssueRecordPaths = (path: string): string[] => {\n  const key = keyFromPath(path)\n  const uuid = uuidFromPath(path)\n  return [\n    ...(key && ISSUE_KEY_PATTERN.test(key) ? [linearByIdPath(key)] : []),\n    ...(uuid ? [linearByUuidPath(uuid)] : []),\n  ].filter((candidate) => candidate !== path)\n}

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: eb3a1e8653

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

for (const candidate of canonicalIssueRecordPaths(path)) {
try {
const canonical = await mount.readFile(candidate)
const parsed = parseLinearIssue(path, canonical.content)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Use canonical content for real writeback guards

When the primary issue file is a stub, this parses the by-id/by-uuid content but deliberately keeps issue.path pointing at the stub primary. In a real dispatch (dryRun: false), MountLinearWriteback.canonicalForIssue re-reads issue.path for its guard (src/writeback/linear.ts:166-179) and rejects that stub because it still has no title/team, so postComment/setState fail even though triage passed; the added dry-run path won't catch this production failure. The fallback needs to make the writeback guard read/accept the canonical payload as well, or real stub-primary issues remain undispatchable.

Useful? React with 👍 / 👎.

@agent-relay-code

Copy link
Copy Markdown
Contributor

Review: PR #10fix/factory-canonical-fallback-canary

Summary

The PR adds tolerance for sparse/stub synced Linear issues via readLinearIssueWithCanonicalFallback (reads by-id/by-uuid canonical aliases when the primary <key>__<uuid>.json parses as a change-event stub), wires a new fleet factory canary CLI command + evaluateFactoryCanary regression detector, and extends stateResolutionFromIds to accept role names for name→UUID backfill. Scope is coherent and matches the stated purpose.

Verification (ran the full CI pipeline locally)

  • npm ci — installed cleanly (396 packages).
  • npm run build (tsc -p tsconfig.build.json && tsc-alias) — passed, no type errors.
  • npm test (vitest run) — 512/512 passed, including the new triages a stub-primary issue by reading the canonical by-id record regression test.
  • npm pack --dry-runpassed.

Traced the diff across callers/types:

  • IterationReport (src/types.ts:135) fields used by evaluateFactoryCanary (pulled/triaged/dispatched/skipped, .issue.key) all type-check; matches({key}) is satisfied by IssueRef, TriageDecision.issue, and DispatchResult.issue.
  • stateResolutionFromIds(stateIds, stateNames) already accepts the optional 2nd arg (src/linear/state-resolver.ts:61); the 2-arg call at factory.ts:267 is valid and the global-only fallback still builds the name→id map.
  • Helpers keyFromPath/uuidFromPath/isMissingIssueFileError exist and behave correctly for the primary-path inputs canonicalIssueRecordPaths derives from.
  • CLI wiring (fleet.ts:139, :341, :364) reaches runFactoryCommand; the canary branch runs before readIssueArg, so it doesn't require the arg to be a readable file.
  • Safety review: the fallback is fail-safe — a missing alias is tolerated and the original stub is returned (factory.ts:329); isUsableIssueRecord gates on real state presence; no fail-closed→fail-open changes; no lifecycle/reaper/in-flight/dispatch code touched.

Findings

No blocking issues. No mechanical (lint/format/typo/import-order) fixes were needed — the diff is clean. I made no file edits; the working tree is unchanged except a pre-existing untracked memory/ dir unrelated to this PR.

Addressed comments

  • No bot or human review comments were present in the provided PR context (.workforce/ contains only pr.diff, changed-files.txt, context.json; no comments file, and context.json lists no review threads). Nothing to reconcile.

Advisory Notes

  • (Non-blocking, do not fold into this PR) evaluateFactoryCanary's arg-key parsing (fleet.ts diff lines 84-86) duplicates the logic in the existing exported keyFromPath (factory.ts:4384). Reusing keyFromPath would remove the divergence risk, but it lives in orchestrator/factory.ts and the dedupe is out of scope for this change — left as-is.

The PR builds, all tests pass, and the change is logically sound and safety-preserving. CI status (mergeability, required checks) is a post-harness action I cannot confirm from here, so I am not declaring it human-ready.

@khaliqgant khaliqgant merged commit 8d1597f into main Jun 18, 2026
3 checks passed
@khaliqgant khaliqgant deleted the fix/factory-canonical-fallback-canary branch June 18, 2026 13:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant